/**
 * UninaDocentiSync - A Java application to synchronize Unina resources
 * Copyright (C) 2011  Fabio Scippacercola (nonplay.programmer@gmail.com)
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA 02111-1307, USA.
 */

package uninadocenti.framework.synch;

import java.beans.PropertyChangeEvent;
import java.io.IOException;
import java.util.ArrayList;
import java.util.EventObject;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import uninadocenti.framework.Insegnamento;
import uninadocenti.framework.WebDocentiFile;
import uninadocentisync.ApplicationUtils;
import uninadocentisync.UninaDocentiSyncApp;

/**
 *  
 * @author user
 */
public class SynchManager {

    public SynchManager() {
        this.addSynchManagerListener(new SynchManagerListener() {
            public void FileSyncStateChanged(PropertyChangeEvent evt) {
                if (ApplicationUtils.getInstance().isDebuggingEnabled())
                    ApplicationUtils.getInstance().log(Level.FINE, String.format("SynchManager <PropertyChangeEvent>: %s, %s -> %s", evt.getPropertyName(), evt.getOldValue().toString(), evt.getNewValue().toString()), null);
            }

            public void DownloadingTreeChanged(EventObject evt) {
                if (ApplicationUtils.getInstance().isDebuggingEnabled())
                    ApplicationUtils.getInstance().log(Level.FINE, "SynchManager <DownloadingTreeChangedEvent>", null);
            }

            public void synchUpdate(SynchUpdateEvent evt) {
                if (evt.eventType == SynchUpdateEvent.EventType.LOG_MSG) {
                    final SynchUpdateEvent.LogMessagePayload p = (SynchUpdateEvent.LogMessagePayload)evt.payload;
                    ApplicationUtils.getInstance().log(p.lr.getLevel(), p.lr.getMessage(), p.lr.getThrown());
                }
                else if (ApplicationUtils.getInstance().isDebuggingEnabled()) {
                    ApplicationUtils.getInstance().log(Level.FINE, String.format("SynchUpdateEvent <%s>: ", evt.eventType.toString(), evt.payload.toString()), null);
                }
            }
        });
    }


    protected void setState(SynchManager.State newState)
    {
        if (this.state != State.INTERRUPTED && this.state != State.ERROR && newState != this.state)
        {
            final State oldState = this.state;
            this.state = newState;
            fireFileSyncStateChangedEvent(new PropertyChangeEvent(this, PROP_STATE, oldState, newState));
        }
    }

    public void interrupt()
    {
        setState(State.INTERRUPTED);
    }

    public boolean isInterrupted()
    {
        return (this.state == State.INTERRUPTED || this.state == State.ERROR);
    }

    public int getFileDownloaded() {
        return fileDownloaded;
    }

    public int getFileToDownload() {
        return fileToDownload;
    }

    public long getTotalBytesDownloaded() {
        return totalBytesDownloaded;
    }

    public State getState()
    {
        return state;
    }

    public SynchingFile<WebDocentiFile> getRoot() {
        return root;
    }
    
    public static enum State
    {
        IDLE, ANALYZING, READY, DOWNLOADING, ERROR, INTERRUPTED, COMPLETED
    }

    public void startDownload()
    {
        try
        {
            setState(State.DOWNLOADING);

            executors = java.util.concurrent.Executors.newFixedThreadPool(NUM_DOWNLOADING_THREADS);
            SynchingFile<WebDocentiFile> n = root;

upWhile:    while (!isInterrupted() && n != null && !n.isProcessed()) {
                if (n.hasChildren())
                {
                    for (SynchingFile<WebDocentiFile> f : n.getChildren()) {
                        if (!f.isProcessed()) {
                        n = f;
                        continue upWhile;
                    }}
                    
                }
                if (n.isRoot()) break; // Raggiunta radice

                processResource(n);
                n = n.getFather();
            }
            executors.shutdown();
            while (!executors.isTerminated())
                executors.awaitTermination(3, TimeUnit.SECONDS);

            setState(State.COMPLETED);
        }
        catch (IOException e)
        {
            // TODO
//                logger.log(Level.SEVERE, String.format("ERRORE: impossibile scaricare le risorse!\n%s!", e.getMessage()));
            setState(State.ERROR);
        }
        catch (InterruptedException e)
        {
            // TODO
//                logger.log(Level.INFO, e.getMessage());
            setState(State.ERROR);
        }
        finally
        {
            executors.shutdownNow();
        }
    }

    public UninaDocentiSyncApp getSyncApp() {
        return syncApp;
    }

    public void setSyncApp(UninaDocentiSyncApp syncApp) {
        this.syncApp = syncApp;
    }

    private SynchingFile<WebDocentiFile> root;
    private UninaDocentiSyncApp syncApp;
    private ExecutorService executors;

    private volatile int fileToDownload, fileDownloaded;
    private volatile long totalBytesDownloaded;
    private volatile State state = State.IDLE;
    final private Object statLock = new Object();

    private final static int NUM_DOWNLOADING_THREADS = 5;
    public static final String PROP_STATE = "state";


    protected volatile ArrayList<SynchManagerListener> synchManagerListeners = new ArrayList<SynchManagerListener>();

    public synchronized void addSynchManagerListener(SynchManagerListener listener) {
        if (!synchManagerListeners.contains(listener)) {
            ArrayList<SynchManagerListener> newLst = (ArrayList<SynchManagerListener>)synchManagerListeners.clone();
            newLst.add(listener);
            synchManagerListeners = newLst;
        }
    }

    public void removeSynchManagerListener(SynchManagerListener listener) {
        ArrayList<SynchManagerListener> newLst = (ArrayList<SynchManagerListener>)synchManagerListeners.clone();
        newLst.remove(listener);
        synchManagerListeners = newLst;
    }

    protected void fireSynchUpdatedEvent(SynchUpdateEvent evt) {
        List<SynchManagerListener> lst = synchManagerListeners;
        for (SynchManagerListener l : lst)
            l.synchUpdate(evt);
    }

    protected void fireDownloadingTreeChangedEvent() {
        List<SynchManagerListener> lst = synchManagerListeners;
        for (SynchManagerListener l : lst)
            l.DownloadingTreeChanged(new java.util.EventObject(this));
    }

    protected void fireFileSyncStateChangedEvent(PropertyChangeEvent evt) {
        List<SynchManagerListener> lst = synchManagerListeners;
        for (SynchManagerListener l : lst)
            l.FileSyncStateChanged(evt);
    }
    
    public void createList(Insegnamento[] insList) throws IOException
    {
        try
        {
            final SynchingFile<WebDocentiFile>[] c = new SynchingFile[insList.length];
            root = new SynchingFile<WebDocentiFile>(null, SynchingFile.State.TODOWNLOAD, null);
            final uninadocenti.framework.WebDocentiUnina conn = syncApp.getWebDocenti();

            setState(State.ANALYZING);

            for (int i = 0; i < insList.length; i++)
                c[i] = doDFS(conn.getResourceAssociated(insList[i]), root);
            root.addChildren(c);

            setState(State.READY);
            fireDownloadingTreeChangedEvent();
        }
        catch (InterruptedException e)
        {
            fireSynchUpdatedEvent(new SynchUpdateEvent(this, SynchUpdateEvent.EventType.LOG_MSG,
                                    new SynchUpdateEvent.LogMessagePayload(null, Level.INFO, e.getMessage(), e)));
            setState(State.ERROR);
        }
    }

    protected SynchingFile<WebDocentiFile> doDFS(WebDocentiFile f, SynchingFile<WebDocentiFile> father)
            throws IOException, InterruptedException
    {
        if (isInterrupted())
            throw new InterruptedException("L'operazione è stata interrotta!");
        if (!f.isDirectory())
        {

            SynchingFile<WebDocentiFile> s = new SynchingFile<WebDocentiFile>(f, SynchingFile.State.TODOWNLOAD, father);
            if (!syncApp.isOverwriteFiles() && getLocalFile(s).exists())
                s.setDownloadState(SynchingFile.State.TOSKIP);

            return s;
        }

        final List<WebDocentiFile> r = syncApp.getWebDocenti().getResourceAssociated(f);
        final SynchingFile<WebDocentiFile>[] c = new SynchingFile[r.size()];

        final SynchingFile<WebDocentiFile> me = new SynchingFile<WebDocentiFile>(f, SynchingFile.State.TODOWNLOAD, father);

        fireSynchUpdatedEvent(new SynchUpdateEvent(this, SynchUpdateEvent.EventType.FOLDER_VISITED,
                                    new SynchUpdateEvent.FolderVisitedPayload(f.getName())));

        for (int i = 0; i < c.length; i++)
            c[i] = doDFS(r.get(i), me);
        me.addChildren(c);
        return me;
    }


    protected void processResource(final SynchingFile<WebDocentiFile> f) throws IOException, InterruptedException
    {
        if (f.getDownloadState() == SynchingFile.State.TOSKIP) {
            f.setDownloadState(SynchingFile.State.SKIPPED);
            fireSynchUpdatedEvent(new SynchUpdateEvent(this, SynchUpdateEvent.EventType.NODE_UPDATE,
                    new SynchUpdateEvent.NodeUpdatePayload(f, SynchingFile.State.TOSKIP, SynchingFile.State.SKIPPED, 0)));
        }
        else if (f.getDownloadState() == SynchingFile.State.TODOWNLOAD)
        {
            // Per il runnable setta la risorsa come in processo (Downloading)
            f.setDownloadState(SynchingFile.State.DOWNLOADING);
            fireSynchUpdatedEvent(new SynchUpdateEvent(this, SynchUpdateEvent.EventType.NODE_UPDATE,
                                new SynchUpdateEvent.NodeUpdatePayload(f, SynchingFile.State.TODOWNLOAD, SynchingFile.State.DOWNLOADING, 0)));

            // Predispone azione da eseguire sul file da parte degli executors
            if(!f.getResource().isDirectory())
            {
                synchronized (statLock) {
                    fileToDownload++;
                }

                executors.submit(new Runnable() {
                    public void run() {
                        int bytesDownloaded = 0;

                        if (isInterrupted()) return;
                        try
                        {
                             bytesDownloaded = downloadFile(f);
                             f.setDownloadState(SynchingFile.State.DOWNLOADED);
                             fireSynchUpdatedEvent(new SynchUpdateEvent(this, SynchUpdateEvent.EventType.NODE_UPDATE,
                                    new SynchUpdateEvent.NodeUpdatePayload(f, SynchingFile.State.DOWNLOADING, SynchingFile.State.DOWNLOADED, bytesDownloaded)));
                        }
                        catch (IOException e)
                        {
                            fireSynchUpdatedEvent(new SynchUpdateEvent(this, SynchUpdateEvent.EventType.LOG_MSG,
                                    new SynchUpdateEvent.LogMessagePayload(f, Level.SEVERE, String.format("ERRORE: impossibile scaricare le risorse!\n%s!", e.getMessage()), e)));
                            setState(State.ERROR);
                        }

                        if (bytesDownloaded == 0)
                        {
                            fireSynchUpdatedEvent(new SynchUpdateEvent(this, SynchUpdateEvent.EventType.LOG_MSG,
                                    new SynchUpdateEvent.LogMessagePayload(f, Level.WARNING, String.format("ATTENZIONE: il file %s risulta vuoto!!", f.getResource().getPath()), null)));
                        }

                        synchronized (statLock)
                        {
                            totalBytesDownloaded += bytesDownloaded;
                            fileDownloaded++;
                        }
                    }
                });
            }
        }
    }


    protected java.io.File getLocalFile(SynchingFile<WebDocentiFile> f)
    {
        return new java.io.File(syncApp.getFileFolder().getAbsolutePath()
                + java.io.File.separator + f.getResource().getPath());
    }

    protected int downloadFile(SynchingFile<WebDocentiFile> f)
            throws IOException
    {
        java.io.File dest = getLocalFile(f);

        if (!dest.exists())
        {
            dest.getParentFile().mkdirs();
            dest.createNewFile();
        }

        java.io.InputStream in = syncApp.getWebDocenti().openResourceStream(f.getResource());
        java.io.OutputStream out = new java.io.FileOutputStream(dest);

        byte[] buf = new byte[1024];
        int len = Integer.MAX_VALUE, tot = 0;
        try
        {
            while (!isInterrupted() && (len = in.read(buf)) > 0)
            {
                out.write(buf, 0, len);
                tot += len;
            }
            return tot;
        }
        finally
        {
            in.close();
            out.close();
            
            if (len > 0) dest.delete(); // Se download interrotto!
        }
    }
}
